package cc.decat.escript.codegen;

import cc.decat.escript.ir.ChunkNode;
import cc.decat.escript.ir.expression.AnonymousFunctionExpression;
import cc.decat.escript.ir.expression.LiteralExpression;
import cc.decat.escript.ir.statement.FunctionDeclarationStatement;
import cc.decat.escript.runtime.GlobalContext;
import cc.decat.escript.runtime.Instruction;
import cc.decat.escript.runtime.Opcode;
import jdk.nashorn.internal.ir.FunctionNode;
import jdk.nashorn.internal.runtime.GlobalFunctions;

public class Codegen {
    private final GlobalContext globalContext;
    private CompileUnit compileUnit = null;
    private final FunctionGenerate globalFunctionGenerate;
    private final BlockGenerate globalBlockGenerate;
    private FunctionGenerate chunkFunctionGenerate;
    private BlockGenerate chunkBlockGenerate;
    private FunctionGenerate functionGenerate;
    private BlockGenerate blockGenerate;
    private BlockGenerate.Type nextBlockType = BlockGenerate.Type.NORMAL;

    public Codegen(GlobalContext globalContext) {
        this.globalContext = globalContext;
        this.globalBlockGenerate = new BlockGenerate(null);
        this.globalBlockGenerate.setSymbolTable(globalContext.getGlobalSymbolTable());
        this.globalFunctionGenerate = new FunctionGenerate("__GLOBAL_FUNCTION__", globalBlockGenerate);
        globalContext.setGlobalFunctionGenerate(globalFunctionGenerate);
    }

    public void addInstruction(Opcode opcode) {
        addInstruction(opcode, -1);
    }

    public void addInstruction(Opcode opcode, int operand) {
        Instruction instruction = new Instruction(opcode, operand);
        functionGenerate.getInstructionList().add(instruction);
    }

    public void loadConst(LiteralExpression literalExpression) {
        int index = -1;
        switch(literalExpression.getType()) {
            case Integer: {
                index = globalContext.getConstGenerate().getIntegerConstIndex(literalExpression.getIntegerValue());
                break;
            }
            case Float: {
                index = globalContext.getConstGenerate().getFloatConstIndex(literalExpression.getFloatValue());
                break;
            }
            case String: {
                index = globalContext.getConstGenerate().getStringConstIndex(literalExpression.getStringValue());
                break;
            }
            default: break;
        }
        addInstruction(Opcode.LOAD_CONST, index);
    }

    public void loadVar(String name) {
        VarInfo varInfo = blockGenerate.getSymbolTable().findVar(name);
        if(varInfo == null) {
            System.out.println("can't find name: " + name);
            System.exit(-1);
        }
        if(varInfo instanceof LocalVarInfo) {
            addInstruction(Opcode.LOAD_LOCAL_VAR, varInfo.getIndex());
        } else if(varInfo instanceof UpVarInfo) {
            addInstruction(Opcode.LOAD_UP_VAR, varInfo.getIndex());
        }
    }

    public void storeVar(String name) {
        VarInfo varInfo = blockGenerate.getSymbolTable().findVar(name);
        if(varInfo == null) {
            addInstruction(Opcode.STORE_LOCAL_VAR, blockGenerate.getSymbolTable().addLocalVar(name));
            return;
        }
        if(varInfo instanceof LocalVarInfo) {
            addInstruction(Opcode.STORE_LOCAL_VAR, varInfo.getIndex());
        } else if(varInfo instanceof UpVarInfo) {
            addInstruction(Opcode.STORE_UP_VAR, varInfo.getIndex());
        }
    }

    public FunctionGenerate getChunkFunctionGenerate() {
        return chunkFunctionGenerate;
    }

    public void generateChunk(ChunkNode chunkNode) {
        BlockGenerate chunkFunctionBlockGenerate = new BlockGenerate(globalBlockGenerate);
        FunctionGenerate chunkFunctionGenerate = new FunctionGenerate("__CHUNK_FUNCTION__$" + this.globalFunctionGenerate.getChildFunctionGenerateList().size(), chunkFunctionBlockGenerate);
        chunkFunctionGenerate.setParent(this.globalFunctionGenerate);
        int index = globalFunctionGenerate.getChildFunctionGenerateList().size();
        globalFunctionGenerate.getChildFunctionGenerateList().add(chunkFunctionGenerate);
        chunkFunctionGenerate.setIndexOfParent(index);
        this.chunkBlockGenerate = this.blockGenerate = chunkFunctionBlockGenerate;
        this.chunkFunctionGenerate = this.functionGenerate = chunkFunctionGenerate;
        System.out.println("entry chunk");
    }

    public void entryBlock() {
        int index = this.blockGenerate.getChildBlockGenerateList().size();
        BlockGenerate newBlockGenerate = new BlockGenerate(this.blockGenerate);
        newBlockGenerate.setType(nextBlockType);
        this.blockGenerate.getChildBlockGenerateList().add(newBlockGenerate);
        this.blockGenerate = newBlockGenerate;
        addInstruction(Opcode.ENTRY_BLOCK, index);
        nextBlockType = BlockGenerate.Type.NORMAL;
    }

    public void leaveBlock() {
        this.blockGenerate = this.blockGenerate.getParent();
        addInstruction(Opcode.LEAVE_BLOCK);
    }

    public FunctionGenerate getFunctionGenerate() {
        return functionGenerate;
    }

    public BlockGenerate getBlockGenerate() {
        return blockGenerate;
    }

    public void generateEntryFunction(FunctionDeclarationStatement functionDeclarationStatement) {
        System.out.println("generate function: " + functionDeclarationStatement.getName());
        FunctionGenerate childFuncGen = new FunctionGenerate(functionDeclarationStatement.getName(), new BlockGenerate(blockGenerate));
        childFuncGen.setParent(this.functionGenerate);
        blockGenerate.getSymbolTable().findOrAddLocalVar(functionDeclarationStatement.getName());
        int childFuncIndex = functionGenerate.getChildFunctionGenerateList().size();
        functionGenerate.getChildFunctionGenerateList().add(childFuncGen);
        childFuncGen.setIndexOfParent(childFuncIndex);
        addInstruction(Opcode.MAKE_FUNCTION, childFuncIndex);
        this.blockGenerate = childFuncGen.getBodyBlockGenerate();
        this.functionGenerate = childFuncGen;
        for(String paramName : functionDeclarationStatement.getParams()) {
            this.blockGenerate.getSymbolTable().addLocalVar(paramName);
        }
    }

    public void generateLeaveFunction() {
        if(this.functionGenerate.getInstructionList().size() < 1
                || this.functionGenerate.getInstructionList().get(this.functionGenerate.getInstructionList().size() - 1).getOpcode() != Opcode.RETURN_VALUE) {
            addInstruction(Opcode.LOAD_NULL);
            addInstruction(Opcode.RETURN_VALUE);
        }
        this.functionGenerate = this.functionGenerate.getParent();
        this.blockGenerate = this.blockGenerate.getParent();
    }

    public String generateEntryLambda(AnonymousFunctionExpression anonymousFunctionExpression) {
        FunctionDeclarationStatement functionDeclarationStatement = new FunctionDeclarationStatement();
        String lambdaName = "Lambda$" +  functionGenerate.getChildFunctionGenerateList().size();
        functionDeclarationStatement.setName(lambdaName);
        functionDeclarationStatement.setBlock(anonymousFunctionExpression.getBlock());
        functionDeclarationStatement.setParams(anonymousFunctionExpression.getParams());
        this.generateEntryFunction(functionDeclarationStatement);
        return lambdaName;
    }

    public void generateLeaveLambda() {
        this.generateLeaveFunction();

    }

    public int mark() {
        return this.functionGenerate.getInstructionList().size();
    }

    public Instruction getInstruction(int index) {
        return this.functionGenerate.getInstructionList().get(index);
    }

    public void setNextBlockType(BlockGenerate.Type nextBlockType) {
        this.nextBlockType = nextBlockType;
    }
}
